/**
 * Code template comes from Eclipse plug-in "Bytecode"
 */
package com.googlecode.cswish.struts.spring.generator;

import static com.googlecode.cswish.asm.Opcodes.ACC_PRIVATE;
import static com.googlecode.cswish.asm.Opcodes.ACC_PUBLIC;
import static com.googlecode.cswish.asm.Opcodes.ALOAD;
import static com.googlecode.cswish.asm.Opcodes.GETFIELD;
import static com.googlecode.cswish.asm.Opcodes.INVOKESPECIAL;
import static com.googlecode.cswish.asm.Opcodes.PUTFIELD;
import static com.googlecode.cswish.asm.Opcodes.RETURN;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.googlecode.cswish.asm.AnnotationVisitor;
import com.googlecode.cswish.asm.ClassWriter;
import com.googlecode.cswish.asm.Label;
import com.googlecode.cswish.asm.MethodVisitor;
import com.googlecode.cswish.asm.Type;
import com.googlecode.cswish.struts.spring.NameType;

/**
 * Generate a value object to wrap a business method. it also supports the Map interface (@see MapWrapHelper)
 * to improve the performance of Gett/Set
 * 	 
 * @author Jerry.feng Date: 10/09/2008
 */
public class ValueObjectHelper {

	private static final Logger logger = Logger.getLogger(ValueObjectHelper.class);

	public static void defineValueObject(String classDefName, String superClassDefName, List<NameType> properties, ClassWriter cw) {
		String classDefNameDesc = "L" + classDefName + ";";
		
		// 2. add field definitions: private String f1;
		if (properties != null && properties.size() > 0) {
			for (NameType nameType : properties) {
				cw.visitField(ACC_PRIVATE, nameType.name, Type.getDescriptor(nameType.type), null, null);
			}
		}
		
		// 3. define constructor
		defineConstructor(classDefNameDesc, superClassDefName, cw);
		
		// 3. add get/set method
		if (properties != null && properties.size() > 0) {
			defineGetSetMethod(classDefName, classDefNameDesc, properties, cw);
		}
	}

	private static String capitalize(String name) {
		if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
                Character.isUpperCase(name.charAt(0))){
		    return name;
		}
		return StringUtils.capitalize(name);
	}
	
	private static void defineGetSetMethod(String classDefName,
			String classDefNameDesc, List<NameType> properties, ClassWriter cw) {
		// TODO: support generic type
		
		for (NameType field : properties) {
			String capName = capitalize(field.name);
			String fieldNameDesc = Type.getDescriptor(field.type);
			
			// getXXX
			{
				String getMethodName = "get" + capName;
				Type returnType = Type.getTypeByDesc(fieldNameDesc);
				Type[] parameterTypes = new Type[] {};
				String getMethodDesc = Type.getMethodDescriptor(
						returnType,parameterTypes);
				String getSignDesc = ValueObjectIntfHelper.getSign(returnType, field.genericType, null, null);
				
				MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, getMethodName, getMethodDesc, getSignDesc, null);
				// annotations
				if (field.annotations != null && field.annotations.length > 0) {
					defineAnnotation(field, mv);
				}
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitVarInsn(ALOAD, 0);
				mv.visitFieldInsn(GETFIELD, classDefName, field.name, fieldNameDesc);
				MethodVisitorHelper.visitReturn(mv, field.type);
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitLocalVariable("this", classDefNameDesc, null, l0, l1, 0);
				mv.visitMaxs(0, 0);
				mv.visitEnd();
			}
			
			
			if (!field.disableSetter)
			// setXXX
			{
				String setMethodName = "set" + capName;
				Type returnType = Type.getType(Void.TYPE);
				Type[] parameterTypes = new Type[] {Type.getTypeByDesc(fieldNameDesc)};
				String setMethodDesc = Type.getMethodDescriptor(
						returnType, parameterTypes);
				String[] genericArgumentTypes = null;
				if (field.genericType != null) {
					genericArgumentTypes = new String[] {field.genericType};
				}
				String setSignDesc = ValueObjectIntfHelper.getSign(returnType, null, parameterTypes, genericArgumentTypes);
				
				MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, setMethodName, setMethodDesc, setSignDesc, null);
				// annotations
				if (field.annotations != null && field.annotations.length > 0) {
					defineAnnotation(field, mv);
				}
				mv.visitCode();
				Label l0 = new Label();
				mv.visitLabel(l0);
				mv.visitVarInsn(ALOAD, 0);
				MethodVisitorHelper.visitLoad(mv, 1, field.type);
				mv.visitFieldInsn(PUTFIELD, classDefName, field.name, fieldNameDesc);
				
				// test code
//				mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//				mv.visitLdcInsn("setMethod is called");
//				mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
				
				Label l1 = new Label();
				mv.visitLabel(l1);
				mv.visitInsn(RETURN);
				Label l2 = new Label();
				mv.visitLabel(l2);
				mv.visitLocalVariable("this", classDefNameDesc, null, l0, l2, 0);
				mv.visitLocalVariable(field.name, fieldNameDesc, null, l0, l2, 1);
				mv.visitMaxs(0, 0);
				mv.visitEnd();
			}
		}
	}

	private static void defineAnnotation(NameType field, MethodVisitor mv) {
		for (Annotation annotation : field.annotations) {
			Class<?> annotationType = annotation.annotationType();
			String annotaionDesc = Type.getDescriptor(annotationType);
			AnnotationVisitor av0 = mv.visitAnnotation(annotaionDesc, true);
			Method[] methods = annotationType.getDeclaredMethods();
			for (Method method : methods) {
				String name = method.getName();
				try {
					Object[] NULL = null;
					Object value = method.invoke(annotation, NULL);
					if (method.getReturnType().isEnum()) {
						av0.visitEnum(name, Type.getDescriptor(method.getReturnType()), value.toString());
					} else {
						av0.visit(name, value);
					}
				} catch (Exception ex) {
					logger.error("Can't add annotation value:" + name, ex);
				}
			}
			av0.visitEnd();
		}
	}

	private static void defineConstructor(String classDefNameDesc, String superClassDefName,
			ClassWriter cw) {
		MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
		mv.visitCode();
		Label l0 = new Label();
		mv.visitLabel(l0);
		mv.visitVarInsn(ALOAD, 0);
		mv.visitMethodInsn(INVOKESPECIAL, superClassDefName, "<init>", "()V");
		mv.visitInsn(RETURN);
		Label l1 = new Label();
		mv.visitLabel(l1);
		mv.visitLocalVariable("this", classDefNameDesc, null, l0, l1, 0);
		mv.visitMaxs(0, 0);
		mv.visitEnd();
	}
}